package com.ideaaedi.zoo.example.springboot.bpmn.flowable.controller;

import cn.hutool.core.collection.ConcurrentHashSet;
import cn.hutool.core.io.resource.ResourceUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.IdUtil;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONWriter;
import com.google.common.collect.Lists;
import com.ideaaedi.zoo.diy.artifact.flowable.convert.BpmnEntityConvert;
import com.ideaaedi.zoo.diy.artifact.flowable.facade.*;
import jakarta.annotation.Resource;
import lombok.SneakyThrows;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Triple;
import org.flowable.bpmn.model.BpmnModel;
import org.flowable.bpmn.model.FlowElement;
import org.flowable.bpmn.model.Process;
import org.flowable.bpmn.model.UserTask;
import org.flowable.common.engine.impl.identity.Authentication;
import org.flowable.engine.HistoryService;
import org.flowable.engine.RepositoryService;
import org.flowable.engine.RuntimeService;
import org.flowable.engine.TaskService;
import org.flowable.engine.history.HistoricProcessInstanceQuery;
import org.flowable.engine.repository.Deployment;
import org.flowable.engine.runtime.ProcessInstance;
import org.flowable.task.api.history.HistoricTaskInstanceQuery;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.ByteArrayInputStream;
import java.util.*;
import java.util.concurrent.ThreadLocalRandom;
import java.util.stream.Collectors;

/**
 *
 */
@RestController
@RequestMapping("/demo-查询")
public class Demo3常见查询Controller {
    
    private static final Set<String> deploymentIdSet = new ConcurrentHashSet<>();
    
    private static final String TENANT = "1,";
    
    private static final String LOCAL_TASK_COMPLETED_BY = "LOCAL_TASK_COMPLETED_BY";
    
    private static final String GLOBAL_PROCESS_STATE_KEY = "PROCESS_STATE";
    private static final String yiFaQiState = "已发起";
    private static final String yiShenQing = "已申请";
    private static final String shenPiTongGuo = "审批通过";
    private static final String shenPiBuTongGuo = "审批不通过";
    private static final String fuShenTongGuo = "复审通过";
    private static final String fuShenBuTongGuo = "复审不通过";

    @Resource
    private HistoryService historyService;

    @Resource
    private RepositoryService repositoryService;

    @Resource
    private RuntimeService runtimeService;

    @Resource
    private TaskService taskService;
    
    private static final String ZS_USER_ID = "张三";
    private static final String LS_USER_ID = "李四";
    private static final String WW_USER_ID = "王五";
    
    @SneakyThrows
    @RequestMapping("/常见查询/待处理任务")
    public void demo1() {
        List<String> infoList;
        try {
            // 0. 先清一下数据，避免脏数据影响测试
            //clearData(); // 这将删除所有部署及相关数据，请确保你连接的是自己的demo数据库
            
            // 1.准备测试数据（此时都尚未处理完成任务）
            infoList = prepareData(null, null, null, false); // 本demo不关注最后一个字段，true/false均可
            
            /*
             * 2.查询张三的待处理
             * 因为bpmn中指定的flowable:assignee办理人就是ZS_USER_ID，所以这里直接查ZS_USER_ID即可
             */
            List<? extends BpmnTask> list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskAssignee(ZS_USER_ID)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());

            infoList.add(String.format("此时查询%s的待处理：", ZS_USER_ID) + formatStr(list));
            
            // 3.张三完成任务
            if (!ZS_USER_ID.equals(list.get(0).getAssignee())) {
                // 任务完成前，将任务完成人设置为任务办理人，方便后续查询已完成的任务(P.S.当然，如果你不想修改assignee的话，你也可以借助任务本地变量来实现， 见this.prepareData方法中的示例)
                taskService.setAssignee(list.get(0).getId(), ZS_USER_ID);
            }
            taskService.complete(list.get(0).getId(), ZS_USER_ID, null);
            
            // 4.再次查询张三的待处理
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskAssignee(ZS_USER_ID)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("张三完成任务后，再查询%s的待处理（发现没有了！）：", ZS_USER_ID) + formatStr(list));
            
            /*
             * 5.查询李四1、李四2的待处理
             * 因为bpmn中指定的【flowable:candidateUsers="李四1,李四2"】办理人候选人组是'李四1,李四2'，所以这里他两应该都能查到这个
             * 待处理任务才对
             */
            String lisi1 = "李四1";
            String lisi2 = "李四2";
            List<? extends BpmnTask> lisi1TaskList = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskCandidateUser(lisi1)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(lisi1TaskList.size() == 1, "存在脏数据！先清除解开步骤0的代码，清除脏数据后再测试");
            BpmnTask lisi1Task = lisi1TaskList.get(0);
            BpmnTask lisi2Task = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskCandidateUser(lisi2)
                            .list()
            ).map(x -> x.stream()
                            .map(BpmnEntityConvert::convert2BpmnTask)
                            .toList())
                    .orElse(Collections.emptyList())
                    .get(0);

            Assert.isTrue(lisi1Task.getId().equals(lisi2Task.getId()), "李四1和李四2看到的任务应该是同一个！");
            infoList.add(String.format("张三完成任务后，%s和%s能同时看到同一个待处理任务：", lisi1, lisi2) + formatStr(lisi1Task));
            
            
            // 6.李四2 完成任务
            if (!lisi2.equals(lisi2Task.getAssignee())) {
                // 任务完成前，将任务完成人设置为任务办理人，方便后续查询已完成的任务(P.S.当然，如果你不想修改assignee的话，你也可以借助任务本地变量来实现， 见this.prepareData方法中的示例)
                taskService.setAssignee(lisi2Task.getId(), lisi2);
            }
            // 先认领任务（认领任务可避免并发冲突），再完成任务
            taskService.claim(lisi2Task.getId(), lisi2);
            taskService.complete(lisi2Task.getId(), lisi2, null);
            
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskCandidateUser(lisi2)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(CollectionUtils.isEmpty(list), "李四2已经完成了该任务，此时他应该查询不到该任务了才对！");
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskAssignee(lisi1)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(CollectionUtils.isEmpty(list), "李四2已经完成了该任务，此时李四1也应该查询不到该任务了才对！");
            infoList.add("李四2完成任务后，李四2和李四1都查询不到该任务了（符合预期）：" + formatStr(list));
            
            /*
             * 7.查询 用户组【DEPT-A】或【ROLE-B】的待处理
             * 因为bpmn中指定的【flowable:candidateGroups="DEPT-A,ROLE-B"】办理人候选组是【DEPT-A】或【ROLE-B】，所以这里在这两个组织下的人员都能看到才对
             */
            // 假设王五在这些组织下（其中有ROLE-B，在候选组里面）
            List<String> wangWuRoles = Lists.newArrayList("ROLE-B", "xyz", "qwer");
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskCandidateGroupIn(wangWuRoles)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(!CollectionUtils.isEmpty(list), "王五拥有【ROLE-B】，照理说能查询出来任务才对！");
            infoList.add("李四2完成任务后，任务到王五这里了：" + formatStr(list));
        } finally {
            // 清除测试数据
            clearData();
        }
        int size = infoList.size();
        for (int i = 1; i <= size; i++) {
            System.err.println("信息" + i + ": " + infoList.get(i - 1));
            System.err.println();
        }
    }
    
    /**
     * <pre>
     * demo1存在的问题：查询还需要根据用户是（办理人、还是候选人、还是候选组）来分别调
     *                用不同的查询方法；但是很多时候，用户本身是不知道自己是什么身份
     *                的，所以这里就需要一个全查的方法了。
     *
     *          需要这样一个方法：只要用户是 办理人 or 候选人 or 在候选组里面， 那
     *          么就能查出来。 这个方法就是：
     *          {@code
     *               taskQuery.or();
     *                 taskQuery.taskAssignee(userId);
     *                 taskQuery.taskCandidateUser(userId);
     *                 taskQuery.taskCandidateGroupIn(userGroups);
     *              taskQuery.endOr();
     *          }
     * </pre>
     */
    @SneakyThrows
    @RequestMapping("/常见查询/待处理任务-全场景查询")
    public void demo2() {
        List<String> infoList;
        try {
            // 0. 先清一下数据，避免脏数据影响测试
            //clearData(); // 这将删除所有部署及相关数据，请确保你连接的是自己的demo数据库
            
            // 1.准备测试数据（此时都尚未处理完成任务）
            infoList = prepareData(null, null, null, true);// 本demo不关注最后一个字段，true/false均可
            
            /*
             * 2.查询张三的待处理
             * 因为bpmn中指定的flowable:assignee办理人就是ZS_USER_ID，所以这里直接查ZS_USER_ID即可
             */
            List<? extends BpmnTask> list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .or()
                            .taskAssignee(ZS_USER_ID)
                            .taskCandidateUser(ZS_USER_ID)
                            .endOr()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());

            infoList.add(String.format("此时查询%s的待处理：", ZS_USER_ID) + formatStr(list));
            
            // 3.张三完成任务
            if (!ZS_USER_ID.equals(list.get(0).getAssignee())) {
                // 任务完成前，将任务完成人设置为任务办理人，方便后续查询已完成的任务(P.S.当然，如果你不想修改assignee的话，你也可以借助任务本地变量来实现， 见this.prepareData方法中的示例)
                taskService.setAssignee(list.get(0).getId(), ZS_USER_ID);
            }
            taskService.complete(list.get(0).getId(), ZS_USER_ID, null);
            
            // 4.再次查询张三的待处理
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .or()
                            .taskAssignee(ZS_USER_ID)
                            .taskCandidateUser(ZS_USER_ID)
                            .endOr()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("张三完成任务后，再查询%s的待处理（发现没有了！）：", ZS_USER_ID) + formatStr(list));
            
            /*
             * 5.查询李四1、李四2的待处理
             * 因为bpmn中指定的【flowable:candidateUsers="李四1,李四2"】办理人候选人组是'李四1,李四2'，所以这里他两应该都能查到这个
             * 待处理任务才对
             */
            String lisi1 = "李四1";
            String lisi2 = "李四2";
            List<? extends BpmnTask> lisi1TaskList = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .or()
                            .taskAssignee(lisi1)
                            .taskCandidateUser(lisi1)
                            .endOr()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(lisi1TaskList.size() == 1, "存在脏数据！先清除解开步骤0的代码，清除脏数据后再测试");
            BpmnTask lisi1Task = lisi1TaskList.get(0);
            BpmnTask lisi2Task = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .or()
                            .taskAssignee(lisi2)
                            .taskCandidateUser(lisi2)
                            .endOr()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList())
             .get(0);
            Assert.isTrue(lisi1Task.getId().equals(lisi2Task.getId()), "李四1和李四2看到的任务应该是同一个！");
            infoList.add(String.format("张三完成任务后，%s和%s能同时看到同一个待处理任务：", lisi1, lisi2) + formatStr(lisi1Task));
            
            
            // 6.李四2 完成任务
            if (!lisi2.equals(lisi2Task.getAssignee())) {
                // 任务完成前，将任务完成人设置为任务办理人，方便后续查询已完成的任务(P.S.当然，如果你不想修改assignee的话，你也可以借助任务本地变量来实现， 见this.prepareData方法中的示例)
                taskService.setAssignee(lisi2Task.getId(), lisi2);
            }
            
            // 先认领任务（认领任务可避免并发冲突），再完成任务
            taskService.claim(lisi2Task.getId(), lisi2);
            taskService.complete(lisi2Task.getId(), lisi2, null);
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .or()
                            .taskAssignee(lisi2)
                            .taskCandidateUser(lisi2)
                            .endOr()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(CollectionUtils.isEmpty(list), "李四2已经完成了该任务，此时他应该查询不到该任务了才对！");
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .or()
                            .taskAssignee(lisi1)
                            .taskCandidateUser(lisi1)
                            .endOr()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(CollectionUtils.isEmpty(list), "李四2已经完成了该任务，此时李四1也应该查询不到该任务了才对！");
            infoList.add("李四2完成任务后，李四2和李四1都查询不到该任务了（符合预期）：" + formatStr(list));
            
            /*
             * 7.查询 用户组【DEPT-A】或【ROLE-B】的待处理
             * 因为bpmn中指定的【flowable:candidateGroups="DEPT-A,ROLE-B"】办理人候选组是【DEPT-A】或【ROLE-B】，所以这里在这两个组织下的人员都能看到才对
             */
            // 假设王五在这些组织下（其中有ROLE-B，在候选组里面）
            List<String> wangWuRoles = Lists.newArrayList("ROLE-B", "xyz", "qwer");
            list = Optional.ofNullable(
                    taskService.createTaskQuery()
                            .taskTenantIdLike(TENANT + "%")
                            .taskCandidateGroupIn(wangWuRoles)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnTask)
                    .toList()
            ).orElse(Collections.emptyList());
            Assert.isTrue(!CollectionUtils.isEmpty(list), "王五拥有【ROLE-B】，照理说能查询出来任务才对！");
            infoList.add("李四2完成任务后，任务到王五这里了：" + formatStr(list));
        } finally {
            // 清除测试数据
            clearData();
        }
        int size = infoList.size();
        for (int i = 1; i <= size; i++) {
            System.err.println("信息" + i + ": " + infoList.get(i - 1));
            System.err.println();
        }
    }
    
    @SneakyThrows
    @RequestMapping("/常见查询/已处理任务")
    public void demo3() {
        List<String> infoList;
        try {
            // 0. 先清一下数据，避免脏数据影响测试
            //clearData(); // 这将删除所有部署及相关数据，请确保你连接的是自己的demo数据库
            
            boolean useAssigneeRecordCompletedBy = ThreadLocalRandom.current().nextBoolean();
            
            // 1.准备测试数据（此时都已完成任务）
            infoList = prepareData(ZS_USER_ID, LS_USER_ID, WW_USER_ID, useAssigneeRecordCompletedBy);
            
            if (useAssigneeRecordCompletedBy) {
                infoList.add("上面的步骤中，当用户完成任务时， 使用的是【将任务完成人赋值给Assignee】的方式，来记录任务完成人，那么下面查询的时候就要用对应的查询条件");
            } else {
                infoList.add("上面的步骤中，当用户完成任务时， 使用的是【将任务完成人记录到任务本地变量】的方式，来记录任务完成人，那么下面查询的时候就要用对应的查询条件");
            }
            
            /*
             * 2.查询张三的已处理
             */
            HistoricTaskInstanceQuery historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery();
            historicTaskInstanceQuery
                    .taskTenantIdLike(TENANT + "%")
                    .finished()
                    .includeTaskLocalVariables()
                    .orderByHistoricTaskInstanceEndTime().desc();
            if (useAssigneeRecordCompletedBy) {
                historicTaskInstanceQuery.taskAssignee(ZS_USER_ID);
            } else {
                historicTaskInstanceQuery.taskVariableValueEquals(LOCAL_TASK_COMPLETED_BY, ZS_USER_ID);
            }
            List<? extends BpmnHisTask> list = Optional.ofNullable(
                    historicTaskInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisTask)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("此时查询%s的已处理：", ZS_USER_ID) + formatStr(list));
            
            /*
             * 3.查询李四的已处理
             */
            historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery()
                    .taskTenantIdLike(TENANT + "%")
                    .finished()
                    .includeTaskLocalVariables()
                    .orderByHistoricTaskInstanceEndTime().desc();
            if (useAssigneeRecordCompletedBy) {
                historicTaskInstanceQuery.taskAssignee(LS_USER_ID);
            } else {
                historicTaskInstanceQuery.taskVariableValueEquals(LOCAL_TASK_COMPLETED_BY, LS_USER_ID);
            }
            list = Optional.ofNullable(
                    historicTaskInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisTask)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("此时查询%s的已处理：", LS_USER_ID) + formatStr(list));
            
            /*
             * 4.查询王五的已处理
             */
            historicTaskInstanceQuery = historyService.createHistoricTaskInstanceQuery()
                    .taskTenantIdLike(TENANT + "%")
                    .finished()
                    .includeTaskLocalVariables()
                    .orderByHistoricTaskInstanceEndTime().desc();
            if (useAssigneeRecordCompletedBy) {
                historicTaskInstanceQuery.taskAssignee(WW_USER_ID);
            } else {
                historicTaskInstanceQuery.taskVariableValueEquals(LOCAL_TASK_COMPLETED_BY, WW_USER_ID);
            }
            list = Optional.ofNullable(
                    historicTaskInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisTask)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("此时查询%s的已处理：", WW_USER_ID) + formatStr(list));
        } finally {
            // 清除测试数据
            clearData();
        }
        int size = infoList.size();
        for (int i = 1; i <= size; i++) {
            System.err.println("信息" + i + ": " + infoList.get(i - 1));
            System.err.println();
        }
    }
    
    @SneakyThrows
    @RequestMapping("/常见查询/已发起流程")
    public void demo4() {
        List<String> infoList = new ArrayList<>();
        try {
            // 0. 先清一下数据，避免脏数据影响测试
            //clearData(); // 这将删除所有部署及相关数据，请确保你连接的是自己的demo数据库
            
            // 1.准备测试数据（发起多个流程）
            prepareData(ZS_USER_ID, LS_USER_ID, WW_USER_ID, true);  // 本demo不关注最后一个字段，true/false均可
            prepareData(ZS_USER_ID, LS_USER_ID, null, true);
            prepareData(ZS_USER_ID, null, null, true);
            
            // 2. 查询已发起流程
            List<? extends BpmnHisProcessInstance> list = Optional.ofNullable(
                    historyService.createHistoricProcessInstanceQuery()
                            .processInstanceTenantIdLike(TENANT + "%")
                            // 对应发起流程时，需要设置流程发起人（即： BpmnProcessService.startByKey(...)时，参数userId）， 要不然这里查询不到
                            .startedBy(ZS_USER_ID)
                            .includeProcessVariables()
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("此时查询%s的已发起流程：", ZS_USER_ID) + formatStr(list));
        } finally {
            // 清除测试数据
            clearData();
        }
        int size = infoList.size();
        for (int i = 1; i <= size; i++) {
            System.err.println("信息" + i + ": " + infoList.get(i - 1));
            System.err.println();
        }
    }
    
    @SneakyThrows
    @RequestMapping("/常见查询/流程进度详情-不含未知节点")
    public void demo5() {
        List<Object> infoList = new ArrayList<>();
        try {
            // 0. 先清一下数据，避免脏数据影响测试
            //clearData(); // 这将删除所有部署及相关数据，请确保你连接的是自己的demo数据库
            
            // 1.准备测试数据（发起多个流程）
            if (ThreadLocalRandom.current().nextBoolean()) {
                prepareData(ZS_USER_ID, LS_USER_ID, WW_USER_ID, true); // 本demo不关注最后一个字段，true/false均可
            } else {
                prepareData(ZS_USER_ID, LS_USER_ID, null, true); // 本demo不关注最后一个字段，true/false均可
            }
            
            // 2. 查询已发起流程
            List<? extends BpmnHisProcessInstance> list = Optional.ofNullable(
                    historyService.createHistoricProcessInstanceQuery()
                            .processInstanceTenantIdLike(TENANT + "%")
                            // 对应发起流程时，需要设置流程发起人（即： BpmnProcessService.startByKey(...)时，参数userId）， 要不然这里查询不到
                            .startedBy(ZS_USER_ID)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());

            BpmnHisProcessInstance bpmnHisProcessInstance = list.get(0);
            String processInstanceId = bpmnHisProcessInstance.getId();
            String processName = ZS_USER_ID + "发起的【"+ bpmnHisProcessInstance.getProcessDefinitionName() + "】流程";
            
            infoList.add(String.format("流程：%s， 流程实例id：%s", processName,
                    processInstanceId));
    
            processDetail(infoList, processInstanceId, false);
        } finally {
            // 清除测试数据
            clearData();
        }
        int size = infoList.size();
        for (int i = 1; i <= size; i++) {
            System.err.println("信息" + i + ": " + infoList.get(i - 1));
            System.err.println();
        }
    }
    
    
    
    @SneakyThrows
    @RequestMapping("/常见查询/流程进度详情-含未知节点")
    public void demo6() {
        List<Object> infoList = new ArrayList<>();
        try {
            // 0. 先清一下数据，避免脏数据影响测试
            //clearData(); // 这将删除所有部署及相关数据，请确保你连接的是自己的demo数据库
        
            // 1.准备测试数据（发起多个流程）
            prepareData(ZS_USER_ID, null, null, true); // 本demo不关注最后一个字段，true/false均可
        
            // 2. 查询已发起流程
            List<? extends BpmnHisProcessInstance> list = Optional.ofNullable(
                    historyService.createHistoricProcessInstanceQuery()
                            .processInstanceTenantIdLike(TENANT + "%")
                            // 对应发起流程时，需要设置流程发起人（即： BpmnProcessService.startByKey(...)时，参数userId）， 要不然这里查询不到
                            .startedBy(ZS_USER_ID)
                            .list()
            ).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());

            BpmnHisProcessInstance bpmnHisProcessInstance = list.get(0);
            String processDefinitionId = bpmnHisProcessInstance.getProcessDefinitionId();
            String processInstanceId = bpmnHisProcessInstance.getId();
            String processName = ZS_USER_ID + "发起的【"+ bpmnHisProcessInstance.getProcessDefinitionName() + "】流程";
        
            infoList.add(String.format("流程：%s， 流程实例id：%s", processName,
                    processInstanceId));
    
            processDetailContainTodoNode(infoList, processDefinitionId, processInstanceId, false);
        } finally {
            // 清除测试数据
            clearData();
        }
        int size = infoList.size();
        for (int i = 1; i <= size; i++) {
            System.err.println("信息" + i + ": " + infoList.get(i - 1));
            System.err.println();
        }
    }
    
    /**
     * 流程进度详情-含未知节点
     */
    public void processDetailContainTodoNode(List<Object> infoList, String processDefinitionId, String processInstanceId,
                                             boolean structRecord) {
        Triple<List<? extends BpmnHisTask>, Map<String, ? extends List<? extends BpmnComment>>, Map<String, ?
                extends List<? extends BpmnAttachment>>> triple = processInfo(processInstanceId);
        List<? extends BpmnHisTask> bpmnHisTaskList = triple.getLeft();
        Map<String, ? extends List<? extends BpmnComment>> taskIdAndCommentsMap = triple.getMiddle();
        Map<String, ? extends List<? extends BpmnAttachment>> taskIdAndAttachmentsMap = triple.getRight();
        
        // -------------------------- 已知的节点 --------------------------
        for (int i = 0; i < bpmnHisTaskList.size(); i++) {
            BpmnHisTask bpmnHisTask = bpmnHisTaskList.get(i);
            String taskId = bpmnHisTask.getId();
            if (structRecord) {
                infoList.add(
                        MapUtil.ofEntries(
                                MapUtil.entry("已知节点", i + 1),
                                MapUtil.entry("任务信息", bpmnHisTask),
                                MapUtil.entry("评论信息", taskIdAndCommentsMap.get(taskId)),
                                MapUtil.entry("附件信息", taskIdAndAttachmentsMap.get(taskId)),
                                MapUtil.entry("任务状态", (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成"))
                        )
                );
            } else {
                infoList.add(String.format("【已知节点-%s】：", (i + 1)) +
                        "任务信息：" + formatStr(bpmnHisTask)
                        + "\n 评论信息：" + formatStr(taskIdAndCommentsMap.get(taskId))
                        + "\n 附件信息：" + formatStr(taskIdAndAttachmentsMap.get(taskId))
                        + "\n 任务状态：" + (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成")
                );
            }
        }
        
        Set<String> knownTaskDefKeySet = bpmnHisTaskList.stream().map(BpmnTask::getTaskDefinitionKey).collect(Collectors.toSet());
        
        
        // -------------------------- 未知的节点（已执行或执行中的任务为已知节点，后续的为未知节点） --------------------------
        List<UserTask> bpmn20UserTasks = queryUserTaskFromBpmnModel(TENANT,
                processDefinitionId, null);
        
        int count = 1;
        for (UserTask bpmn20UserTask : bpmn20UserTasks) {
            boolean contains = knownTaskDefKeySet.contains(bpmn20UserTask.getId());
            if (contains) {
                continue;
            }
            String assignee = bpmn20UserTask.getAssignee();
            List<String> candidateUsers = bpmn20UserTask.getCandidateUsers();
            List<String> candidateGroups = bpmn20UserTask.getCandidateGroups();
            String description = bpmn20UserTask.getDocumentation();
            String name = bpmn20UserTask.getName();
            if (structRecord) {
                infoList.add(
                        MapUtil.ofEntries(
                                MapUtil.entry("未知节点", count + ""),
                                MapUtil.entry("任务名", name),
                                MapUtil.entry("任务描述", StringUtils.defaultIfBlank(description, "（无）")),
                                MapUtil.entry("办理人", StringUtils.defaultIfBlank(assignee, "（未指定）")),
                                MapUtil.entry("候选人", CollectionUtils.isEmpty(candidateUsers) ? "（无）" : String.join("、", candidateUsers)),
                                MapUtil.entry("候选组", CollectionUtils.isEmpty(candidateGroups) ? "（无）" : String.join("、", candidateGroups))
                        )
                );
            } else {
                infoList.add(
                        String.format("未知节点-%s：\n\t\t任务名：%s\n\t\t任务描述:%s\n\t\t办理人：%s\n\t\t候选人：%s\n\t\t候选组：%s",
                                count,
                                name,
                                StringUtils.defaultIfBlank(description, "（无）"),
                                StringUtils.defaultIfBlank(assignee, "（未指定）"),
                                CollectionUtils.isEmpty(candidateUsers) ? "（无）" : String.join("、", candidateUsers),
                                CollectionUtils.isEmpty(candidateGroups) ? "（无）" : String.join("、", candidateGroups)
                        )
                );
            }
            
            count++;
        }
    }
    
    /**
     * 流程进度详情-不含未知节点
     */
    public  void processDetail(List<Object> infoList, String processInstanceId, boolean structRecord) {
        Triple<List<? extends BpmnHisTask>, Map<String, ? extends List<? extends BpmnComment>>, Map<String, ?
                extends List<? extends BpmnAttachment>>> triple = processInfo(processInstanceId);
        List<? extends BpmnHisTask> bpmnHisTaskList = triple.getLeft();
        Map<String, ? extends List<? extends BpmnComment>> taskIdAndCommentsMap = triple.getMiddle();
        Map<String, ? extends List<? extends BpmnAttachment>> taskIdAndAttachmentsMap = triple.getRight();
        
        for (int i = 0; i < bpmnHisTaskList.size(); i++) {
            BpmnHisTask bpmnHisTask = bpmnHisTaskList.get(i);
            String taskId = bpmnHisTask.getId();
            if (structRecord) {
                infoList.add(
                        MapUtil.ofEntries(
                                MapUtil.entry("节点", i + 1),
                                MapUtil.entry("任务信息", bpmnHisTask),
                                MapUtil.entry("评论信息", taskIdAndCommentsMap.get(taskId)),
                                MapUtil.entry("附件信息", taskIdAndAttachmentsMap.get(taskId)),
                                MapUtil.entry("任务状态", (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成"))
                        )
                );
            } else {
                infoList.add(String.format("【节点%s】：", (i + 1)) +
                        "任务信息：" + formatStr(bpmnHisTask)
                        + "\n 评论信息：" + formatStr(taskIdAndCommentsMap.get(taskId))
                        + "\n 附件信息：" + formatStr(taskIdAndAttachmentsMap.get(taskId))
                        + "\n 任务状态：" + (bpmnHisTask.getEndTime() == null ? "待处理" : "已完成")
                );
            }
        }
    }
    
    /**
     *
     * @param recordProcessStateByBusinessState 是否已流程business-status字段记录流程状态
     * <br /><br />
     * true-通过流程的business-status记录流程状态；<br />
     * false-通过流程变量记录流程状态
     */
    @SneakyThrows
    @RequestMapping("/常见查询/按流程状态查询流程")
    public void demo7(boolean recordProcessStateByBusinessState) {
        String faQiRen = "流程发起人Deng";
        List<String> infoList = new ArrayList<>();
        try {
            // 0. 先清一下数据，避免脏数据影响测试
            //clearData(); // 这将删除所有部署及相关数据，请确保你连接的是自己的demo数据库
        
            // 1.准备测试数据（发起多个流程）
            // 准备 yiFaQiState 状态的流程
            prepareDataProcessState(faQiRen, null, null, null, null, null, recordProcessStateByBusinessState);
            // 准备 yiShenQing 状态的流程
            prepareDataProcessState(faQiRen, ZS_USER_ID, null, null, null, null, recordProcessStateByBusinessState);
            // 准备 shenPiTongGuo 状态的流程
            prepareDataProcessState(faQiRen, ZS_USER_ID, LS_USER_ID, true, null, null, recordProcessStateByBusinessState);
            // 准备 shenPiBuTongGuo 状态的流程
            prepareDataProcessState(faQiRen, ZS_USER_ID, LS_USER_ID, false, null, null, recordProcessStateByBusinessState);
            // 准备 fuShenTongGuo 状态的流程
            prepareDataProcessState(faQiRen, ZS_USER_ID, LS_USER_ID, true, WW_USER_ID, true, recordProcessStateByBusinessState);
            // 准备 fuShenBuTongGuo 状态的流程
            prepareDataProcessState(faQiRen, ZS_USER_ID, LS_USER_ID, true, WW_USER_ID, false, recordProcessStateByBusinessState);
        
            // 查询 yiFaQiState 状态的流程
            HistoricProcessInstanceQuery historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
            historicProcessInstanceQuery
                    .processInstanceTenantIdLike(TENANT + "%")
                    .startedBy(faQiRen)
                    .includeProcessVariables();
            if (recordProcessStateByBusinessState) {
                historicProcessInstanceQuery.processInstanceBusinessStatus(yiFaQiState);
            } else {
                historicProcessInstanceQuery.variableValueEquals(GLOBAL_PROCESS_STATE_KEY, yiFaQiState);
            }
            List<? extends BpmnHisProcessInstance> list = Optional.ofNullable(historicProcessInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());

            infoList.add(String.format("查询【%s】状态的流程：", yiFaQiState) + formatStr(list));
        
            // 查询 yiShenQing 状态的流程
            historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
            historicProcessInstanceQuery
                    .processInstanceTenantIdLike(TENANT + "%")
                    .startedBy(faQiRen)
                    .includeProcessVariables();
            if (recordProcessStateByBusinessState) {
                historicProcessInstanceQuery.processInstanceBusinessStatus(yiShenQing);
            } else {
                historicProcessInstanceQuery.variableValueEquals(GLOBAL_PROCESS_STATE_KEY, yiShenQing);
            }

            list = Optional.ofNullable(historicProcessInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("查询【%s】状态的流程：", yiShenQing) + formatStr(list));
        
            // 查询 shenPiTongGuo 状态的流程
            historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
            historicProcessInstanceQuery
                    .processInstanceTenantIdLike(TENANT + "%")
                    .startedBy(faQiRen)
                    .includeProcessVariables();
            if (recordProcessStateByBusinessState) {
                historicProcessInstanceQuery.processInstanceBusinessStatus(shenPiTongGuo);
            } else {
                historicProcessInstanceQuery.variableValueEquals(GLOBAL_PROCESS_STATE_KEY, shenPiTongGuo);
            }

            list = Optional.ofNullable(historicProcessInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("查询【%s】状态的流程：", shenPiTongGuo) + formatStr(list));
        
            // 查询 shenPiBuTongGuo 状态的流程
            historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
            historicProcessInstanceQuery
                    .processInstanceTenantIdLike(TENANT + "%")
                    .startedBy(faQiRen)
                    .includeProcessVariables();
            if (recordProcessStateByBusinessState) {
                historicProcessInstanceQuery.processInstanceBusinessStatus(shenPiBuTongGuo);
            } else {
                historicProcessInstanceQuery.variableValueEquals(GLOBAL_PROCESS_STATE_KEY, shenPiBuTongGuo);
            }
            list = Optional.ofNullable(historicProcessInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("查询【%s】状态的流程：", shenPiBuTongGuo) + formatStr(list));
        
            // 查询 fuShenBuTongGuo 状态的流程
            historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
            historicProcessInstanceQuery
                    .processInstanceTenantIdLike(TENANT + "%")
                    .startedBy(faQiRen)
                    .includeProcessVariables();
            if (recordProcessStateByBusinessState) {
                historicProcessInstanceQuery.processInstanceBusinessStatus(fuShenBuTongGuo);
            } else {
                historicProcessInstanceQuery.variableValueEquals(GLOBAL_PROCESS_STATE_KEY, fuShenBuTongGuo);
            }
            list = Optional.ofNullable(historicProcessInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("查询【%s】状态的流程：", fuShenBuTongGuo) + formatStr(list));
        
            // 查询 fuShenTongGuo 状态的流程
            historicProcessInstanceQuery = historyService.createHistoricProcessInstanceQuery();
            historicProcessInstanceQuery
                    .processInstanceTenantIdLike(TENANT + "%")
                    .startedBy(faQiRen)
                    .includeProcessVariables();
            if (recordProcessStateByBusinessState) {
                historicProcessInstanceQuery.processInstanceBusinessStatus(fuShenTongGuo);
            } else {
                historicProcessInstanceQuery.variableValueEquals(GLOBAL_PROCESS_STATE_KEY, fuShenTongGuo);
            }
            list = Optional.ofNullable(historicProcessInstanceQuery.list()).map(x -> x.stream()
                    .map(BpmnEntityConvert::convert2BpmnHisProcessInstance)
                    .toList()
            ).orElse(Collections.emptyList());
            infoList.add(String.format("查询【%s】状态的流程：", fuShenTongGuo) + formatStr(list));
        } finally {
            // 清除测试数据
            clearData();
        }
        int size = infoList.size();
        for (int i = 1; i <= size; i++) {
            System.err.println("信息" + i + ": " + infoList.get(i - 1));
            System.err.println();
        }
    }
    
    /**
     * 准备数据
     *
     * @param zs 申请人。当为空时，表示不进行申请
     * @param ls 审批人。当为空时，表示不进行审批
     * @param ww 复审人。当为空时，表示不进行复审
     * @param useAssigneeRecordCompletedBy true-直接将任务完成人记录到任务办理人字段里；false-使用任务本地变量来记录任务完成人
     *
     * @return 相关信息
     */
    private List<String> prepareData(@Nullable String zs, @Nullable String ls,
                                     @Nullable String ww, boolean useAssigneeRecordCompletedBy) {
        List<String> infoList = new ArrayList<>();
        // ---------- step1. 部署生成 流程定义 ----------
        Deployment deployment = repositoryService.createDeployment().tenantId(TENANT)
                .addBytes("张三李四王五.bpmn20.xml", ResourceUtil.readBytes("张三李四王五.bpmn20.xml"))
                .name("张三李四王五")
                .deploy();
        BpmnDeployment deploy = BpmnEntityConvert.convert2BpmnDeployment(deployment);
        String deploymentId = deploy.getId();
        deploymentIdSet.add(deploymentId);
        infoList.add("部署id：" + deploymentId);
        List<? extends BpmnProcessDefinition> bpmnProcessDefinitions = Optional.ofNullable(
                repositoryService.createProcessDefinitionQuery()
                        .processDefinitionTenantIdLike(TENANT + "%")
                        .deploymentId(deploymentId)
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnProcessDefinition)
                .toList()
        ).orElse(Collections.emptyList());
        infoList.add("流程定义key：" + bpmnProcessDefinitions.get(0).getKey() + "，流程定义id：" + bpmnProcessDefinitions.get(0).getId()); // 因为本bpmn文件中只定义了一个流程，所以这里取第一个即可
        
        // ---------- step2. 根据流程定义发起流程 ----------
        // 流程定义key,即为xxx.bpmn20.xml中的<bpmn:process id="xxx" />中的id。值等价于 bpmnProcessDefinitions.get(0).getKey()
        String processDefinitionKey = "zslsww";
        ProcessInstance processInstance;
        try {
            // Flowable发起流程时，会直接从登录态中获取当前用户作为流程发起人
            Authentication.setAuthenticatedUserId(zs);
            processInstance = runtimeService.startProcessInstanceByKeyAndTenantId(processDefinitionKey, IdUtil.simpleUUID(),
                    null, TENANT);
        } finally {
            Authentication.setAuthenticatedUserId(null);
        }
        BpmnProcessInstance bpmnProcessInstance = BpmnEntityConvert.convert2BpmnProcessInstance(processInstance);

        String bpmnProcessInstanceId = bpmnProcessInstance.getId();
        infoList.add("流程实例id：" + bpmnProcessInstanceId);
        
        // ---------- step3. （流程自动流转，）查看任务 ----------
        List<? extends BpmnTask> list = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(bpmnProcessInstanceId)
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnTask)
                .toList()
        ).orElse(Collections.emptyList());
        infoList.add("发起流程后， 当前任务流转到了：" + formatStr(list));
        
        if (StringUtils.isBlank(zs)) {
            return infoList;
        }
        BpmnTask bpmnTask = list.get(0);
    
        if (!zs.equals(bpmnTask.getAssignee())) {
            // 先申领任务，再完成任务
            taskService.claim(bpmnTask.getId(), zs);
        }
        /*
         * 任务完成前，将任务完成人设置为任务办理人，方便后续查询已完成的任务
         *
         * P.S.当然，如果你不想修改assignee的话，你也可以借助其它方式来实现，如：使用任务本地变量设置完成人id
         */
        if (useAssigneeRecordCompletedBy) {
            if (!zs.equals(bpmnTask.getAssignee())) {
                taskService.setAssignee(bpmnTask.getId(), zs);
            }
        } else {
            taskService.setVariableLocal(bpmnTask.getId(), LOCAL_TASK_COMPLETED_BY, zs);
        }
        taskService.addComment(bpmnTask.getId(), bpmnProcessInstanceId, zs + "的评论：" + "请假拉屎");
        taskService.createAttachment("附件类型1", bpmnTask.getId(), bpmnProcessInstanceId, "附件名1", "附件描述1",
                new ByteArrayInputStream("附件内容1".getBytes()));

        taskService.complete(bpmnTask.getId(), zs, null);
        list = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(bpmnProcessInstanceId)
                        .includeTaskLocalVariables()
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnTask)
                .toList()
        ).orElse(Collections.emptyList());
        infoList.add(String.format("%s完成了申请后， 当前任务流转到了：", zs) + formatStr(list));
        if (StringUtils.isBlank(ls)) {
            return infoList;
        }
    
        bpmnTask = list.get(0);
        if (!ls.equals(bpmnTask.getAssignee())) {
            // 先申领任务，再完成任务
            taskService.claim(bpmnTask.getId(), ls);
        }

        /*
         * 任务完成前，将任务完成人设置为任务办理人，方便后续查询已完成的任务
         *
         * P.S.当然，如果你不想修改assignee的话，你也可以借助其它方式来实现，如：使用任务本地变量设置完成人id
         */
        if (useAssigneeRecordCompletedBy) {
            if (!ls.equals(bpmnTask.getAssignee())) {
                taskService.setAssignee(bpmnTask.getId(), ls);
            }
        } else {
            taskService.setVariableLocal(bpmnTask.getId(), LOCAL_TASK_COMPLETED_BY, ls);
        }
        taskService.addComment(bpmnTask.getId(), bpmnProcessInstanceId, ls + "的评论：" + String.format("打小就看%s这小子行，通过！", zs));
        taskService.createAttachment("附件类型2", bpmnTask.getId(), bpmnProcessInstanceId,
                "附件名2", "附件描述2", new ByteArrayInputStream("附件内容2".getBytes()));
        taskService.complete(bpmnTask.getId(), ls, null);
        list = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(bpmnProcessInstanceId)
                        .includeTaskLocalVariables()
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnTask)
                .toList()
        ).orElse(Collections.emptyList());
        infoList.add(String.format("%s完成了审核后， 当前任务流转到了：", ls) + formatStr(list));
        if (StringUtils.isBlank(ww)) {
            return infoList;
        }
    
        bpmnTask = list.get(0);
        if (!ww.equals(bpmnTask.getAssignee())) {
            // 先申领任务，再完成任务
            taskService.claim(bpmnTask.getId(), ww);
        }
        /*
         * 任务完成前，将任务完成人设置为任务办理人，方便后续查询已完成的任务
         *
         * P.S.当然，如果你不想修改assignee的话，你也可以借助其它方式来实现，如：使用任务本地变量设置完成人id
         */
        if (useAssigneeRecordCompletedBy) {
            if (!zs.equals(bpmnTask.getAssignee())) {
                taskService.setAssignee(bpmnTask.getId(), ww);
            }
        } else {
            taskService.setVariableLocal(bpmnTask.getId(), LOCAL_TASK_COMPLETED_BY, ww);
        }
        taskService.addComment(bpmnTask.getId(), bpmnProcessInstanceId, ww + "的评论：" + "准~");
        taskService.createAttachment("附件类型3",  bpmnTask.getId(), bpmnProcessInstanceId,
                "附件名3",  "附件描述3", new ByteArrayInputStream("附件内容3".getBytes()));
        taskService.complete(bpmnTask.getId(), ww, null);
        list = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(bpmnProcessInstanceId)
                        .includeTaskLocalVariables()
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnTask)
                .toList()
        ).orElse(Collections.emptyList());
        if (CollectionUtils.isEmpty(list)) {
            bpmnProcessInstance = Optional.ofNullable(runtimeService.createProcessInstanceQuery()
                            .processInstanceTenantIdLike(TENANT + "%")
                            .processInstanceId(bpmnProcessInstanceId)
                            .singleResult()
                    ).map(BpmnEntityConvert::convert2BpmnProcessInstance)
                    .orElse(null);
            Assert.isTrue(bpmnProcessInstance == null, "流程实例执行完毕后，bpmnProcessService.detailInstance接口应该就查询不到流程实例信息才对！");
            infoList.add(String.format("%s何完成了审核后， 流程实例执行完毕啦！", ww));
        } else {
            throw new IllegalStateException("按照[张三李四王五.bpmn20.xml]中的定义，流程应该执行完了才对，任务列表因为为空才对！");
        }
        return infoList;
    }
    
    /**
     * 准备流程状态数据
     *
     * @param faQiRen 流程发起人
     * @param zs 申请人。当为空时，表示不进行申请
     * @param ls 审批人。当为空时，表示不进行审批
     * @param lsPass ls是否审核通过
     * @param ww 复审人。当为空时，表示不进行复审
     * @param wwPass ww是否复审通过
     * @param recordProcessStateByBusinessState true-通过流程的business-status记录流程状态；false-通过流程变量记录流程状态
     */
    private void prepareDataProcessState(@Nullable String faQiRen, @Nullable String zs,
                                         @Nullable String ls, @Nullable Boolean lsPass,
                                         @Nullable String ww, @Nullable Boolean wwPass,
                                         boolean recordProcessStateByBusinessState
    ) {
        // ---------- step1. 部署生成 流程定义 ----------
        repositoryService.createDeployment().tenantId(TENANT)
                .addBytes("张三李四王五.bpmn20.xml", ResourceUtil.readBytes("张三李四王五.bpmn20.xml"))
                .name("张三李四王五")
                .deploy();
        
        // ---------- step2. 根据流程定义发起流程 ----------
        // 流程定义key,即为xxx.bpmn20.xml中的<bpmn:process id="xxx" />中的id。值等价于 bpmnProcessDefinitions.get(0).getKey()
        String processDefinitionKey = "zslsww";
        String bpmnProcessInstanceId;
        if (recordProcessStateByBusinessState) {
            ProcessInstance processInstance;
            try {
                // Flowable发起流程时，会直接从登录态中获取当前用户作为流程发起人
                Authentication.setAuthenticatedUserId(faQiRen);
                processInstance = runtimeService.startProcessInstanceByKeyAndTenantId(processDefinitionKey, IdUtil.simpleUUID(),
                        null, TENANT);
            } finally {
                Authentication.setAuthenticatedUserId(null);
            }
            BpmnProcessInstance bpmnProcessInstance = BpmnEntityConvert.convert2BpmnProcessInstance(processInstance);

            bpmnProcessInstanceId = bpmnProcessInstance.getId();
            runtimeService.updateBusinessStatus(bpmnProcessInstanceId, yiFaQiState);
        } else {
            ProcessInstance processInstance;
            try {
                // Flowable发起流程时，会直接从登录态中获取当前用户作为流程发起人
                Authentication.setAuthenticatedUserId(faQiRen);
                processInstance = runtimeService.startProcessInstanceByKeyAndTenantId(processDefinitionKey, IdUtil.simpleUUID(),
                        Map.of(GLOBAL_PROCESS_STATE_KEY, yiFaQiState), TENANT);
            } finally {
                Authentication.setAuthenticatedUserId(null);
            }
            BpmnProcessInstance bpmnProcessInstance = BpmnEntityConvert.convert2BpmnProcessInstance(processInstance);

            bpmnProcessInstanceId = bpmnProcessInstance.getId();
        }
        // ---------- step3. （流程自动流转，）查看任务 ----------
        List<? extends BpmnTask> list = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(bpmnProcessInstanceId)
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnTask)
                .toList()
        ).orElse(Collections.emptyList());

        if (StringUtils.isBlank(zs)) {
            return;
        }
        BpmnTask bpmnTask = list.get(0);
        if (!zs.equals(bpmnTask.getAssignee())) {
            taskService.claim(bpmnTask.getId(), zs); // 先申领任务，再完成任务
            taskService.setAssignee(bpmnTask.getId(), zs);
        }
        
        if (recordProcessStateByBusinessState) {
            runtimeService.updateBusinessStatus(bpmnProcessInstanceId, yiShenQing);
            taskService.complete(bpmnTask.getId(), zs, null);
        } else {
            taskService.complete(bpmnTask.getId(), zs, Map.of(GLOBAL_PROCESS_STATE_KEY, yiShenQing));
        }
        
        if (StringUtils.isBlank(ls)) {
            return;
        }
    
        list = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(bpmnProcessInstanceId)
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnTask)
                .toList()
        ).orElse(Collections.emptyList());
        bpmnTask = list.get(0);
        if (!ls.equals(bpmnTask.getAssignee())) {
            taskService.claim(bpmnTask.getId(), ls); // 先申领任务，再完成任务
            taskService.setAssignee(bpmnTask.getId(), ls);
        }
        
        if (BooleanUtils.isTrue(lsPass)) {
            if (recordProcessStateByBusinessState) {
                runtimeService.updateBusinessStatus(bpmnProcessInstanceId, shenPiTongGuo);
                taskService.complete(bpmnTask.getId(), ls, null);
            } else {
                taskService.complete(bpmnTask.getId(), ls, Map.of(GLOBAL_PROCESS_STATE_KEY, shenPiTongGuo));
            }
        } else {
            if (recordProcessStateByBusinessState) {
                runtimeService.updateBusinessStatus(bpmnProcessInstanceId, shenPiBuTongGuo);
                taskService.complete(bpmnTask.getId(), ls, null);
            } else {
                taskService.complete(bpmnTask.getId(), ls, Map.of(GLOBAL_PROCESS_STATE_KEY, shenPiBuTongGuo));
            }
            // 如果流程图上，设置有 fuShenBuTongGuo 对应的结束事件的话，这里就不用主动删除（推荐此方式）
            long count = runtimeService.createProcessInstanceQuery()
                    .processInstanceId(bpmnProcessInstanceId)
                    .count();
            if (count > 0) {
                runtimeService.deleteProcessInstance(bpmnProcessInstanceId, "流程实例的删除原因是因为审批不通过。");
            }
        }
        if (StringUtils.isBlank(ww)) {
            return;
        }
    
    
        list = Optional.ofNullable(
                taskService.createTaskQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(bpmnProcessInstanceId)
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnTask)
                .toList()
        ).orElse(Collections.emptyList());
        bpmnTask = list.get(0);
        
        if (!ww.equals(bpmnTask.getAssignee())) {
            taskService.claim(bpmnTask.getId(), ww);
            taskService.setAssignee(bpmnTask.getId(), ww);
        }
        if (BooleanUtils.isTrue(wwPass)) {
            if (recordProcessStateByBusinessState) {
                runtimeService.updateBusinessStatus(bpmnProcessInstanceId, fuShenTongGuo);
                taskService.complete(bpmnTask.getId(), ww, null);
            } else {
                taskService.complete(bpmnTask.getId(), ww, Map.of(GLOBAL_PROCESS_STATE_KEY, fuShenTongGuo));
            }
        } else {
            if (recordProcessStateByBusinessState) {
                runtimeService.updateBusinessStatus(bpmnProcessInstanceId, fuShenBuTongGuo);
                taskService.complete(bpmnTask.getId(), ww, null);
            } else {
                taskService.complete(bpmnTask.getId(), ww, Map.of(GLOBAL_PROCESS_STATE_KEY, fuShenBuTongGuo));
            }
            // 如果流程图上，设置有 fuShenBuTongGuo 对应的结束事件的话，这里就不用主动删除（推荐此方式）
            long count = runtimeService.createProcessInstanceQuery()
                    .processInstanceId(bpmnProcessInstanceId)
                    .count();
            if (count > 0) {
                runtimeService.deleteProcessInstance(bpmnProcessInstanceId, "流程实例的删除原因是因为复审不通过。");
            }
        }
    }
    
    /*
     * 清除数据
     */
    private void clearData() {
        List<? extends BpmnDeployment> list = Optional.ofNullable(
                        repositoryService.createDeploymentQuery()
                                .deploymentTenantIdLike(TENANT + "%")
                                .list()
                ).map(x -> x.stream()
                        .map(BpmnEntityConvert::convert2BpmnDeployment)
                        .toList()
                ).orElse(Collections.emptyList());
        for (BpmnDeployment bpmnDeployment : list) {
            repositoryService.deleteDeployment(bpmnDeployment.getId(), true);
        }
    }
    
    private static String formatStr(Object obj) {
        return JSON.toJSONString(obj, JSONWriter.Feature.PrettyFormat);
    }
    
    /**
     * 查询流程相关信息
     *
     * @param processInstanceId 流程实例id
     *
     * @return  左-任务列表；中-评论（key为任务id）；右-附件（key为任务id）
     */
    private Triple<List<? extends BpmnHisTask>,
            Map<String, ? extends List<? extends BpmnComment>>,
            Map<String, ? extends List<? extends BpmnAttachment>>> processInfo(String processInstanceId) {
        // 2. 查询流程各任务节点
        List<? extends BpmnHisTask> bpmnHisTaskList = Optional.ofNullable(
                historyService.createHistoricTaskInstanceQuery()
                        .taskTenantIdLike(TENANT + "%")
                        .processInstanceId(processInstanceId)
                        .orderByHistoricTaskInstanceStartTime().asc()
                        .list()
        ).map(x -> x.stream()
                .map(BpmnEntityConvert::convert2BpmnHisTask)
                .toList()
        ).orElse(Collections.emptyList());
        
        // 流程评论
        List<? extends BpmnComment> processInstanceCommentList = Optional.ofNullable(
                taskService.getProcessInstanceComments(processInstanceId)
                ).map(x -> x.stream().map(BpmnEntityConvert::convert2BpmnComment).collect(Collectors.toList()))
                .orElse(Collections.emptyList());
        
        // 任务评论
        Map<String, ? extends List<? extends BpmnComment>> taskIdAndCommentsMap =
                processInstanceCommentList.stream().filter(x -> Objects.nonNull(x.getTaskId()) && "comment".equals(x.getType()))
                        .collect(Collectors.groupingBy(BpmnComment::getTaskId));
        // 流程附件
        List<? extends BpmnAttachment> processInstanceAttachments = Optional.ofNullable(
                        taskService.getProcessInstanceAttachments(processInstanceId)
                ).map(x -> x.stream().map(BpmnEntityConvert::convert2BpmnAttachment).collect(Collectors.toList()))
                .orElse(Collections.emptyList());
    
        // 任务附件
        Map<String, ? extends List<? extends BpmnAttachment>> taskIdAndAttachmentsMap =
                processInstanceAttachments.stream().filter(x -> Objects.nonNull(x.getTaskId()))
                        .collect(Collectors.groupingBy(BpmnAttachment::getTaskId));
        return Triple.of(bpmnHisTaskList, taskIdAndCommentsMap, taskIdAndAttachmentsMap);
    }

    /**
     * 从bpmn模型中查询流程定义的用户任务
     *
     * @param tenant 租户
     * @param processDefId 流程定义id
     * @param xmlProcessId bpmn模型中的流程id <br />注：bpmn xml文件中，Process元素的id属性值
     *
     * @return 流程定义的用户任务
     */
    @Nonnull
    public List<UserTask> queryUserTaskFromBpmnModel(@Nonnull String tenant, @Nonnull String processDefId,
                                                     @Nullable String xmlProcessId) {
        // 获取流程定义的BpmnModel，其中包含了流程的所有节点信息
        BpmnModel bpmnModel = repositoryService.getBpmnModel(processDefId);
        if (bpmnModel == null) {
            throw new RuntimeException("Not found BpmnModel.");
        }
        // 获取流程定义的主流程
        Process process;
        if (StringUtils.isNotBlank(xmlProcessId)) {
            process = bpmnModel.getProcessById(xmlProcessId);
        } else {
            process = bpmnModel.getMainProcess();
        }
        if (process == null) {
            throw new RuntimeException("Not found Process.");
        }

        List<UserTask> list = new ArrayList<>();
        // 此方法只关注用户任务，其余方法忽略
        for (FlowElement flowElement : process.getFlowElements()) {
            if (flowElement instanceof UserTask userTask) {
                list.add(userTask);
            }
        }
        return list;
    }
}